﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Reflection;

using RpgLibrary.CharacterClasses;

using XRpgLibrary;
using XRpgLibrary.SpriteClasses;
using XRpgLibrary.ItemClasses;
using XRpgLibrary.ExtensionMethods;
using XRpgLibrary.Logging;
using XRpgLibrary.ScriptClasses;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;


namespace XRpgLibrary.CharacterClasses
{
    public class Character : INotifyPropertyChanged
    {
        #region Script Fields and Properties

        private static Assembly scriptAssembly = null;
        private static Assembly ScriptAssembly 
        {
            get
            {
                if (scriptAssembly == null)
                    scriptAssembly = Assembly.GetExecutingAssembly();

                return scriptAssembly;
            }
        }

        private static Type typeCharacter = null;
        private static Type TypeCharacter
        {
            get
            {
                if (typeCharacter == null)
                    typeCharacter = typeof(Character);

                return typeCharacter;
            }
        }

        private static Dictionary<string, EventInfo> characterEventInfo;
        private static Dictionary<string, EventInfo> CharacterEventInfo
        {
            get
            {
                if (characterEventInfo == null)
                {
                    characterEventInfo = BuildCharacterEventInfo();
                }

                return characterEventInfo;
            }
        }

        private List<GameScript> attachedScripts;
        private List<GameScript> AttachedScripts
        {
            get
            {
                if (attachedScripts == null)
                    attachedScripts = new List<GameScript>();

                return attachedScripts;
            }
        }

        public static ScriptContext ScriptContext { get; set; }

        public event GameEventHandler Activate;
        public event GameEventHandler Collide;
        public event GameEventHandler Colliding;
        public event GameEventHandler Collided;
        public event GameEventHandler Containing;
        public event GameEventHandler CollideWall;
        public event GameEventHandler Create;
        public event GameEventHandler Created;
        public event GameEventHandler Destroy;
        public event GameEventHandler Destroyed;
        public event GameEventHandler MapEnter;
        public event GameEventHandler MapLeave;
        public event GameEventHandler PlayerMapEnter;
        public event GameEventHandler PlayerMapLeave;
        public event GameEventHandler CharacterUpdate;

        #endregion

        #region Field Region

        protected Entity entity;
        protected AnimatedSprite sprite;
        protected List<EventScript> scripts;
        int tileSpanX = -1; 
        int tileSpanY = -1;
        bool isWalking = false;
        bool isCollided = false;
        bool isAlive = true;
        double slope = -1;

        public event PropertyChangedEventHandler PropertyChanged;
        
        // Armor fields
        protected GameItem head;
        protected GameItem body;
        protected GameItem hands;
        protected GameItem feet;

        // Weapon/Shield fields
        protected GameItem mainHand;
        protected GameItem offHand;
        protected int handsFree;

        #endregion
        
        #region Property Region

        public string Name
        {
            get
            {
                return Entity.EntityName;
            }
            set
            {
                Entity.EntityName = value;
                NotifyPropertyChanged("Name");
            }
        }

        public Entity Entity
        {
            get { return entity; }
        }
        
        public AnimatedSprite Sprite
        {
            get { return sprite; }
            set { sprite = value; }
        }

        public List<EventScript> Scripts
        {
            get 
            {
                if (scripts == null)
                    scripts = new List<EventScript>();

                return scripts; 
            }
            set 
            { 
                scripts = value; 
            }
        }

        public Vector4 CollisionBox
        {
            get
            {
                return new Vector4(
                    sprite.Position.X, 
                    sprite.Position.Y, 
                    sprite.Height,
                    sprite.Width);
            }
        }

        public Vector4 BlockBox
        {
            get
            {
                return new Vector4(
                    (float)(sprite.Position.X + sprite.Width * .125),
                    (float)(sprite.Position.Y + sprite.Height * .5),
                    (float)(sprite.Height * .25),
                    (float)(sprite.Width * .75));
            }
        }

        public bool PassThrough
        {
            get;
            set;
        }

        public bool RespectPassability
        {
            get;
            set;
        }
        
        public bool IsAlive
        {
            get
            {
                return isAlive;   
            }
            private set
            {
                isAlive = value;
            }
        }

        public bool IsPlayer { get; set; }

        public int TileSpanX
        {
            get
            {
                if (tileSpanX < 0)
                    tileSpanX = TileSpan(this.Sprite.Width, RpgLibrary.RpgConstants.TileWidth);;

                return tileSpanX;
            }
        }

        public int TileSpanY
        {
            get
            {
                if (tileSpanY < 0)
                    tileSpanY = TileSpan(this.Sprite.Height, RpgLibrary.RpgConstants.TileHeight); ;

                return tileSpanY;
            }
        }

        public bool IsWalking 
        { 
            get 
            {
                return isWalking;
            }
            set
            {
                isWalking = value;
                this.Sprite.IsAnimating = value;
            }
            
        }

        public double DirectionCutoffSlope
        {
            get
            {
                if (slope < 0)
                    slope = Math.Tan(Math.PI / RpgLibrary.RpgConstants.WalkDirections);

                return slope;
            }
        }

        private List<Character> collidingWith = null;
        private List<Character> CollidingWith
        {
            get
            {
                if (collidingWith == null)
                    collidingWith = new List<Character>();

                return collidingWith;
            }
        }

        // Armor properties
        public GameItem Head
        {
            get { return head; }
        }
        public GameItem Body
        {
            get { return body; }
        }
        public GameItem Hands
        {
            get { return hands; }
        }
        public GameItem Feet
        {
            get { return feet; }
        }

        // Weapon/Shield properties
        public GameItem MainHand
        {
            get { return mainHand; }
        }
        public GameItem OffHand
        {
            get { return offHand; }
        }
        public int HandsFree
        {
            get {
                handsFree = 0;
                if (mainHand == null)
                    handsFree++;
                if (offHand == null)
                    handsFree++;

                return handsFree; 
            }
        }

        #endregion
        
        #region Constructor Region

        public Character(CharacterData characterData)
            : this(characterData, null, null, null)
        { }

        public Character(
            Character character, 
            ContentManager contentManager, 
            SpriteBatch spriteBatch, 
            ScriptContext scriptContext)
        {
            Construct(character.GetDataObject(), contentManager, spriteBatch, scriptContext);
            this.sprite = character.sprite;
        }

        public Character(
            CharacterData characterData, 
            AnimatedSprite sprite, 
            ContentManager contentManager, 
            SpriteBatch spriteBatch, 
            ScriptContext scriptContext)
        {
            Construct(characterData, contentManager, spriteBatch, scriptContext);
            if (sprite != null)
                this.sprite = sprite;
        }

        public Character(
            CharacterData characterData, 
            SpriteBatch spriteBatch, 
            ContentManager contentManager, 
            ScriptContext scriptContext)
            :this (characterData, null, contentManager, spriteBatch, scriptContext)
        { }

        private void Construct(
            CharacterData characterData, 
            ContentManager contentManager, 
            SpriteBatch spriteBatch, 
            ScriptContext scriptContext)
        {
            this.scripts = characterData.Scripts;
            this.InitializeScriptEvents();

            OnCreate(null);

            this.entity = Entity.FromEntityData(characterData.Entity);
            
            this.sprite = AnimatedSprite.FromAnimatedSpriteData(characterData.Sprite);
            this.sprite.SpriteBatch = spriteBatch;
            this.InitializeAnimations(contentManager);

            this.PassThrough = characterData.PassThrough;
            this.RespectPassability = characterData.RespectPassability;

            OnCreated(null);
        }

        #endregion

        #region Initialize Methods
        
        private void InitializeAnimations(ContentManager contentManager)
        {
            this.Sprite.Texture = contentManager.Load<Texture2D>(this.Sprite.TextureImageName);
        }

        private void InitializeScriptEvents()
        {
            foreach (EventScript eventScript in Scripts)
                AddScriptEvent(eventScript);
        }

        public bool AddScriptEvent(EventScript eventScript)
        {
            return AddScriptEvent(eventScript.EventName, GetScriptInstance(eventScript));
        }

        public bool AddScriptEvent(string eventName, GameScript scriptInstance)
        {
            if (scriptInstance == null)
            {
                Logging.Log.Error("Failed to load the script '{0}' for event '{1}'",
                    scriptInstance.ScriptName,
                    eventName);

                return false;
            }

            scriptInstance.SetIdentity(eventName, scriptInstance.ScriptName);
   
            AttachedScripts.Add(scriptInstance);

            Delegate delegateEvent = GetScriptDelegateEvent(
                eventName,
                scriptInstance);

            MethodInfo addEventHandlerMethod = CharacterEventInfo[eventName].GetAddMethod();
            addEventHandlerMethod.Invoke(this, new Object[] { delegateEvent });

            return true;
        }

        public bool RemoveScriptEvent(string eventName, string scriptName)
        {
            return RemoveScriptEvent(new EventScript(eventName, scriptName, null));
        }
        
        public bool RemoveScriptEvent(EventScript eventScript)
        {
            bool removed = false;
            List<GameScript> removedScripts = new List<GameScript>();
            
            foreach(GameScript gs in AttachedScripts)
                if (gs.Is(eventScript))
                {
                    RemoveScriptEventHandler(eventScript.EventName, gs);
                    removedScripts.Add(gs);
                    removed = true;
                }

            foreach (GameScript gs in removedScripts)
                AttachedScripts.Remove(gs);

            return removed;
        }

        public bool RemoveScriptEvent(GameScript script)
        {
            bool removed = false;
            List<GameScript> removedScripts = new List<GameScript>();
            
            foreach (GameScript gs in AttachedScripts)
                if (gs == script)
                {
                    RemoveScriptEventHandler(gs.EventName, gs);
                    removedScripts.Add(gs);
                    removed = true;
                }

            foreach (GameScript gs in removedScripts)
                AttachedScripts.Remove(gs);

            return removed;
        }

        private void RemoveScriptEventHandler(string eventName, GameScript gs)
        {
            Delegate delegateEvent = GetScriptDelegateEvent(eventName, gs);
            MethodInfo removeEventHandlerMethod = CharacterEventInfo[eventName].GetRemoveMethod();
            removeEventHandlerMethod.Invoke(this, new Object[] { delegateEvent });
        }


        internal GameScript GetScript(string eventName, string scriptName)
        {
            EventScript e = new EventScript(eventName, scriptName, null);
            foreach (GameScript gs in AttachedScripts)
                if (gs.Is(e))
                    return gs;

            return null;
        }

        private static Delegate GetScriptDelegateEvent(string eventName, GameScript scriptInstance)
        {
            //"Execute" is required by GameScript and all script types must implement it
            //so it's gauranteed to be present on this call
            Delegate delegateEvent =
                Delegate.CreateDelegate(
                    CharacterEventInfo[eventName].EventHandlerType,
                    scriptInstance,
                    "Execute");

            return delegateEvent;
        }

        private static GameScript GetScriptInstance(EventScript eventScript)
        {
            Type scriptType = GetScriptType(eventScript.ScriptName);

            ConstructorInfo[] scriptConstructors = scriptType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);

            if (scriptConstructors.Length != 1)
                throw new Exception(
                    string.Format("Malformed Script constructor for type '{0}'. Scripts should define exactly one constructor!",
                        scriptType));

            object[] scriptArgs = eventScript.ScriptArguments;

            if (scriptArgs == null)
                scriptArgs = BuildDefaultArgumentList(scriptType, scriptConstructors[0].GetParameters());

            if (scriptArgs == null)
                return null;

            GameScript scriptInstance = (GameScript)scriptConstructors[0].Invoke(scriptArgs);
            scriptInstance.SetIdentity(eventScript.EventName, eventScript.ScriptName);
            
            return scriptInstance;
        }

        private static object[] BuildDefaultArgumentList(Type typeScript, ParameterInfo[] parameters)
        {
            object[] args = new object[parameters.Length];
            for (int i = 0, cnt = args.Length; i < cnt; i++)
            {
                if (parameters[i].DefaultValue == DBNull.Value)
                {
                    Log.Error("Script Error: The script '{0}' has at least one required argument, '{1}', but no value is defined for it.",
                            typeScript, parameters[i]);
                    return null;
                }
                args[i] = parameters[i].DefaultValue;
            }       

            return args;
        }

        private static Type GetScriptType(string eventScriptName)
        {
            Type typeScript = ScriptAssembly.GetType(eventScriptName);

            if (typeScript == null)
                throw new Exception(
                    string.Format("Script '{0}' is not found!",
                    eventScriptName));

            return typeScript;
        }

        private static Dictionary<string, EventInfo> BuildCharacterEventInfo()
        {
            Dictionary<string, EventInfo> cei = new Dictionary<string, EventInfo>();

            cei.Add("Activate", TypeCharacter.GetEvent("Activate"));
            cei.Add("Collide", TypeCharacter.GetEvent("Collide"));
            cei.Add("Colliding", TypeCharacter.GetEvent("Colliding"));
            cei.Add("Collided", TypeCharacter.GetEvent("Collided"));
            cei.Add("Containing", TypeCharacter.GetEvent("Containing"));
            cei.Add("CollideWall", TypeCharacter.GetEvent("CollideWall"));
            cei.Add("Create", TypeCharacter.GetEvent("Create"));
            cei.Add("Created", TypeCharacter.GetEvent("Created"));
            cei.Add("Destroy", TypeCharacter.GetEvent("Destroy"));
            cei.Add("Destroyed", TypeCharacter.GetEvent("Destroyed"));
            cei.Add("MapEnter", TypeCharacter.GetEvent("MapEnter"));
            cei.Add("MapLeave", TypeCharacter.GetEvent("MapLeave"));
            cei.Add("PlayerMapEnter", TypeCharacter.GetEvent("PlayerMapEnter"));
            cei.Add("PlayerMapLeave", TypeCharacter.GetEvent("PlayerMapLeave"));
            cei.Add("CharacterUpdate", TypeCharacter.GetEvent("CharacterUpdate"));

            return cei;
        }

        #endregion

        #region Virtual Method region
        
        public virtual void Update(GameTime gameTime)
        {
            if (CharacterUpdate != null)
                CharacterUpdate(this, null, ScriptContext, gameTime);

            entity.Update(gameTime.ElapsedGameTime);
            sprite.Update(gameTime);
        }

        public virtual void Draw(GameTime gameTime)
        {
            //DrawPassabilityCalculationShadow();

            sprite.Draw(gameTime);

            //DrawBlockBox();

        }

        /// <summary>
        /// Normally the spriteBatch is set when the character is created, 
        /// but sometimes we want to render to a different render target, 
        /// so this functon lets us specify a different spritBatch
        /// </summary>
        /// <param name="renderTargetSpriteBatch"></param>
        public virtual void Draw(SpriteBatch renderTargetSpriteBatch)
        {
            sprite.Draw(renderTargetSpriteBatch);
        }

        /// <summary>
        /// This is debug method that draws the character's blockBox
        /// </summary>
        private void DrawBlockBox()
        {
            Texture2D texture = new Texture2D(sprite.SpriteBatch.GraphicsDevice, 1, 1);
            texture.SetData<Color>(new Color[] { new Color(0, 0, 255, 25) });

            sprite.SpriteBatch.Draw(
                texture,
                this.BlockBox.ToRectangle(),
                Color.White);
        }

        /// <summary>
        /// This is a debug method that draws a rectangle over the area that's active for this
        /// character's wall passability calculation
        /// </summary>
        private void DrawPassabilityCalculationShadow()
        {
            Point topLeftTile = XRpgLibrary.TileEngine.Engine.VectorToCell(this.Sprite.Position);

            Texture2D texture = new Texture2D(sprite.SpriteBatch.GraphicsDevice, 1, 1);
            texture.SetData<Color>(new Color[] { new Color(0, 255, 0, 25) });

            sprite.SpriteBatch.Draw(
                texture,
                new Rectangle(
                    (int)topLeftTile.X * RpgLibrary.RpgConstants.TileWidth,
                    (int)topLeftTile.Y * RpgLibrary.RpgConstants.TileHeight,
                    TileSpanX * RpgLibrary.RpgConstants.TileWidth,
                    TileSpanY * RpgLibrary.RpgConstants.TileHeight),
                Color.White);
        }

        public virtual bool Equip(GameItem gameItem)
        {
            bool success = false;
            return success;
        }
        public virtual bool Unequip(GameItem gameItem)
        {
            bool success = false;
            return success;
        }

        #endregion

        public int TileSpan(int length, int chunkSize)
        {
            int span = (int)(length / chunkSize) + 1;
            return span < 2 ? 2 : span;
        }

        public void Kill()
        {
            this.IsAlive = false;
        }

        public void Move(Vector2 unnormalizedMotion, float elapsedSeconds)
        {
            if (unnormalizedMotion != Vector2.Zero)
                unnormalizedMotion.Normalize();

            Move(unnormalizedMotion * Sprite.Speed * elapsedSeconds);
        }

        public void Move(Vector2 motion)
        {
            if (motion != Vector2.Zero)
            {
                SetDirection(motion);

                this.IsWalking = true;
                sprite.Position += motion;
                sprite.LockToMap();
            }
            else
            {
                this.IsWalking = false;
            }
        }

        internal void MoveTo(Vector2 destination)
        {
            sprite.Position = destination;
            sprite.LockToMap();
        }

        private void SetDirection(Vector2 motion)
        {
            var s1 = motion.X * DirectionCutoffSlope + motion.Y > 0 ? 0 : 1;
            var s2 = motion.Y * DirectionCutoffSlope + motion.X > 0 ? 0 : 1;
            var s3 = motion.Y * DirectionCutoffSlope - motion.X < 0 ? 0 : 1;
            var s4 = motion.X * DirectionCutoffSlope - motion.Y > 0 ? 0 : 1;

            var segment = 4 * s4 + 2 * (s2 ^ s4) + (s1 ^ s2 ^ s3 ^ s4);

            switch (segment)
            {
                case 0:
                case 1:
                    sprite.CurrentAnimation = AnimationKey.East;
                    break;
                case 2:
                case 3:
                    sprite.CurrentAnimation = AnimationKey.North;
                    break;
                case 4:
                case 5:
                    sprite.CurrentAnimation = AnimationKey.West;
                    break;
                case 6:
                case 7:
                    sprite.CurrentAnimation = AnimationKey.South;
                    break;
                default:
                    sprite.CurrentAnimation = AnimationKey.South;
                    break;
            }
        }

        #region Event Methods

        public void DoCollideWall(Vector4 collisionRect, Vector2 repel, GameTime gameTime)
        {
            if (this.RespectPassability)
                this.Sprite.Position += repel;

            OnCollideWall(collisionRect, gameTime);
        }

        public void DoColliding(Character other, GameTime gameTime)
        {
            if (CollidingWith.Contains(other))
            {
                if (CollisionBox.ToRectangle().Contains(other.CollisionBox.ToRectangle()))
                    OnContaining(other, gameTime);

                if (this.IsWalking && !other.PassThrough)
                    this.Sprite.Position += other.BlockBox.IntersectionRepelVector(this.CollisionBox);

                OnColliding(other, gameTime);
            }
            else
                OnCollide(other, gameTime);
        }

        protected virtual void OnCollide(Character other, GameTime gameTime)
        {
            if (!CollidingWith.Contains(other))
            {    
                CollidingWith.Add(other);
            
                if (Collide != null)
                    Collide(this, other, ScriptContext, gameTime);
            }
        }

        public virtual void DoCollided(Character other, GameTime gameTime)
        {
            if (CollidingWith.Contains(other))
            {
                CollidingWith.Remove(other);
                if (Collided != null)
                    Collided(this, other, ScriptContext, gameTime);
            }
        }

        protected virtual void OnCollideWall(Vector4 wall, GameTime gameTime)
        {
            if (CollideWall != null)
                CollideWall(this, wall, ScriptContext, gameTime);
        }

        protected virtual void OnColliding(Character player, GameTime gameTime)
        {
            if (Colliding != null)
                Colliding(this, player, ScriptContext, gameTime);
        }

        protected virtual void OnContaining(Character player, GameTime gameTime)
        {
            if (Containing != null)
                Containing(this, player, ScriptContext, gameTime);
        }

        protected virtual void OnActivate(Character player, GameTime gameTime)
        {
            if (Activate != null)
                Activate(this, player, ScriptContext, gameTime);
        }

        protected virtual void OnCreate(GameTime gameTime)
        {
            if (Create != null)
                Create(this, null, ScriptContext, gameTime);
        }

        protected virtual void OnCreated(GameTime gameTime)
        {
            if (Created != null)
                Created(this, null, ScriptContext, gameTime);
        }

        protected virtual void OnDestroy(GameTime gameTime)
        {
            if (Destroy != null)
                Destroy(this, null, ScriptContext, gameTime);
        }

        protected virtual void OnDestroyed(GameTime gameTime)
        {
            if (Destroyed != null)
                Destroyed(this, null, ScriptContext, gameTime);
        }

        protected virtual void OnMapEnter(GameTime gameTime)
        {
            if (MapEnter != null)
                MapEnter(this, null, ScriptContext, gameTime);
        }

        protected virtual void OnMapLeave(GameTime gameTime)
        {
            if (MapLeave != null)
                MapLeave(this, null, ScriptContext, gameTime);
        }
        
        #endregion

        #region DataObject Methods

        internal static Character FromCharacterData(
            CharacterData characterData, 
            SpriteBatch spriteBatch, 
            ContentManager contentManager, 
            ScriptContext scriptContext)
        {
            return new Character(characterData, spriteBatch, contentManager, scriptContext);
        }

        public CharacterData GetDataObject()
        {
            return new CharacterData(
                this.Entity.GetDataObject(), 
                this.Sprite.GetDataObject(), 
                this.Scripts,
                this.PassThrough,
                this.RespectPassability);
        }

        #endregion

        private void NotifyPropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }



    }
}